/*
 * Created on 23-Apr-2003
 * Last modified 25-March-2005
 *
 * * Software License
 *
 * Copyright (c) 2003 David Bridgewater.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by
 *        David Bridgewater (http://www.jbridge.co.uk/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL DAVID BRIDGEWATER
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 
 */
package uk.co.jbridge.httpclient;

import java.io.*;
import java.net.*;
import java.util.*;

/**
 * @author David
 * 
 * The HttpClient class estabishes connections with web servers, and processes
 * all seven of the common-or-garden HTTP methods.
 */

public class HttpClient {

    private String theURL;

    private String theHttpMethod;

    private Map requestInfo;

    private Map responseInfo = new HashMap();

    private ArrayList responseBody = new ArrayList();

    private File fileForPut;

    private String dataForPost;

    private int responseCode;

    private HashMap responseCodeMappings = new HashMap();

    private String[] requestHeaders = { "Accept", "Accept-Charset",
            "Accept-Encoding", "Accept-Language", "Authorization", "Expect",
            "From", "Host", "If-Match", "If-Modified-Since", "If-None-Match",
            "If-Range", "If-Unmodified-Since", "Max-Forwards",
            "Proxy-Authorization", "Range", "Referer", "TE", "User-Agent" };

    private String[] responseHeaders = { "Accept-Ranges", "Age", "E-Tag",
            "Location", "Proxy-Authenticate", "Retry-After", "Server", "Vary",
            "WWW-Authenticate" };

    private String[] generalHeaders = { "Cache-Control", "Connection", "Date",
            "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via",
            "Warning" };

    private String[] entityHeaders = { "Allow", "Content-Encoding",
            "Content-Language", "Content-Length", "Content-Location",
            "Content-MD5", "Content-Range", "Content-Type", "Expires",
            "Last-Modified" };

    private HashMap headerMappings = new HashMap();

    /**
     *  
     */
    public HttpClient() {
        super();

        /*
         * An ugly but understandable way to initialise a map containing all the
         * HTTP response codes and their short descriptions.
         */
        // Initialise response code mappings
        // Informational 1xx
        responseCodeMappings.put(new Integer(100), "Continue");
        responseCodeMappings.put(new Integer(101), "Switching Protocols");
        // Successful 2xx
        responseCodeMappings.put(new Integer(200), "OK");
        responseCodeMappings.put(new Integer(201), "Created");
        responseCodeMappings.put(new Integer(202), "Accepted");
        responseCodeMappings.put(new Integer(203),
                "Non-Authoritative Information");
        responseCodeMappings.put(new Integer(204), "No Content");
        responseCodeMappings.put(new Integer(205), "Reset Content");
        responseCodeMappings.put(new Integer(206), "Partial Content");
        // Redirection 3xx
        responseCodeMappings.put(new Integer(300), "Multiple Choices");
        responseCodeMappings.put(new Integer(301), "Moved Permanently");
        responseCodeMappings.put(new Integer(302), "Found");
        responseCodeMappings.put(new Integer(303), "See Other");
        responseCodeMappings.put(new Integer(304), "Not Modified");
        responseCodeMappings.put(new Integer(305), "Use Proxy");
        responseCodeMappings.put(new Integer(306), "[Unused]");
        responseCodeMappings.put(new Integer(307), "Temporary Redirect");
        // Client Error 4xx
        responseCodeMappings.put(new Integer(400), "Bad Request");
        responseCodeMappings.put(new Integer(401), "Unauthorized");
        responseCodeMappings.put(new Integer(402), "Payment Required");
        responseCodeMappings.put(new Integer(403), "Forbidden");
        responseCodeMappings.put(new Integer(404), "Not Found");
        responseCodeMappings.put(new Integer(405), "Method Not Allowed");
        responseCodeMappings.put(new Integer(406), "Not Acceptable");
        responseCodeMappings.put(new Integer(407),
                "Proxy Authentication Required");
        responseCodeMappings.put(new Integer(408), "Request Timeout");
        responseCodeMappings.put(new Integer(409), "Conflict");
        responseCodeMappings.put(new Integer(410), "Gone");
        responseCodeMappings.put(new Integer(411), "Length Required");
        responseCodeMappings.put(new Integer(412), "Precondition Failed");
        responseCodeMappings.put(new Integer(413), "Request Entity Too Large");
        responseCodeMappings.put(new Integer(414), "Request-URI Too Long");
        responseCodeMappings.put(new Integer(415), "Unsupported Media Type");
        responseCodeMappings.put(new Integer(416),
                "Requested Range Not Satisfiable");
        responseCodeMappings.put(new Integer(417), "Expectation Failed");
        // Server Error 5xx
        responseCodeMappings.put(new Integer(500), "Internal Server Error");
        responseCodeMappings.put(new Integer(501), "Not Implemented");
        responseCodeMappings.put(new Integer(502), "Bad Gateway");
        responseCodeMappings.put(new Integer(503), "Service Unavailable");
        responseCodeMappings.put(new Integer(504), "Gateway Timeout");
        responseCodeMappings
                .put(new Integer(505), "HTTP Version Not Supported");

        /*
         * A more elegant way to load a map with header field names and their
         * category - whether request, response, general or entity.
         */
        loadFieldMappings();

    }

    /*
     * The loadFieldMappings() helper method associates all field names with
     * descriptive text for the category of field: whether request, response,
     * general or entity.
     */
    protected void loadFieldMappings() {
        String[] typeDescriptions = { "Request", "Response", "General",
                "Entity" };
        Object[] headerKeyArrays = { requestHeaders, responseHeaders,
                generalHeaders, entityHeaders };
        for (int i = 0; i < typeDescriptions.length; i++) {
            String[] keyArray = (String[]) headerKeyArrays[i];
            for (int j = 0; j < keyArray.length; j++) {
                headerMappings.put(keyArray[j], typeDescriptions[i]);
            }
        }

    }

    public void executeHttpRequest() {
        if ((theURL != null) && theURL.startsWith("http://")) {
            try {
                URL url = new URL(theURL);
                URLConnection connection = url.openConnection();
                if (connection instanceof HttpURLConnection) {
                    HttpURLConnection httpConnection = (HttpURLConnection) connection;
                    httpConnection.setRequestMethod(theHttpMethod);

                    /* Set up some standard request properties */
                    /* httpConnection
                            .setRequestProperty(
                                    "Accept",
                                    "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, " +
                                    "application/vnd.ms-powerpoint, application/msword, application/x-shockwave-flash, * /*"); */
                    httpConnection.addRequestProperty("Accept", "image/gif");
                    httpConnection.addRequestProperty("Accept", "image/x-bitmap");
                    httpConnection.addRequestProperty("Accept", "image/jpeg");
                    httpConnection.addRequestProperty("Accept", "image/pjpeg");
                    httpConnection.addRequestProperty("Accept", "application/vnd.ms-excel");
                    httpConnection.addRequestProperty("Accept", "application/vnd.ms-powerpoint");
                    httpConnection.addRequestProperty("Accept", "application/msword");
                    httpConnection.addRequestProperty("Accept", "application/x-shockwave-flash");
                    httpConnection.addRequestProperty("Accept", "*/*");
                    httpConnection.setRequestProperty("Accept-Language",
                            "en-gb,en-us");
                    httpConnection.setRequestProperty("User-Agent",
                            "Mozilla/4.0");
                    httpConnection.setRequestProperty("If-Modified-Since",
                            "Tue, 15 Nov 1994 08:12:31 GMT");

                    // !!! ADD ADDITIONAL REQUEST PROPERTIES BELOW, FOLLOWING
                    // THE SAME
                    // PATTERN AS PROPERTIES ADDED ABOVE.

                    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

                    // Load a map with request header fields
                    requestInfo = httpConnection.getRequestProperties();

                    // Special processing for PUT method requests
                    if (theHttpMethod.equalsIgnoreCase("PUT")) {
                        executePutRequest(httpConnection);
                    }

                    // Special processing for POST method requests
                    if (theHttpMethod.equalsIgnoreCase("POST")) {
                        executePostRequest(httpConnection);
                    }

                    // Load a map with response header fields
                    loadResponseInfo(httpConnection);

                    // Make status code accessible
                    responseCode = httpConnection.getResponseCode();

                    // Load an array list with the response body
                    loadResponseBody(httpConnection);

                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /*
     * The method loadResponseBody() loads an array list with the response body,
     * when present. Note that this could be a response body containing error
     * text (because the real resource could not be returned in the response
     * body for some reason).
     */
    protected void loadResponseBody(HttpURLConnection httpConnection)
            throws IOException {
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(
                    httpConnection.getInputStream()));
            for (String line = null; (line = br.readLine()) != null;) {
                responseBody.add(line);
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }

        // If no response body - could be because an error was returned.
        // Populate the response body with this instead.
        if (responseBody.size() == 0
                && (httpConnection.getResponseCode() > 400)) {
            BufferedReader br = new BufferedReader(new InputStreamReader(
                    httpConnection.getErrorStream()));
            for (String line = null; (line = br.readLine()) != null;) {
                responseBody.add(line);
            }
        }
    }

    /*
     * The loadResponseInfo() helper method takes response header fields and
     * coerces them into a modifiable map.
     */
    protected void loadResponseInfo(HttpURLConnection httpConnection) {
        // Load a map with response header fields
        Map headerFields = httpConnection.getHeaderFields();
        // Copy the unmodifiable map of header fields to a modifiable map -
        // so we can add more stuff to it.
        Set s = headerFields.keySet();
        Iterator i = s.iterator();
        while (i.hasNext()) {
            Object o = i.next();
            // Remove the spurious "null, null" response header
            // that creeps in somehow.
            String str = "" + o;
            if (!str.equals("null")) {
                responseInfo.put(o, headerFields.get(o));
            }
        }
    }

    /*
     * The executePostRequest() helper method processes data for posting, making
     * this available to the web server in a parameter called "postData".
     */
    protected void executePostRequest(HttpURLConnection httpConnection)
            throws IOException {
        httpConnection.setDoOutput(true);
        OutputStream os = httpConnection.getOutputStream();
        DataOutputStream dos = new DataOutputStream(os);
        dos.writeBytes("postData=" + dataForPost);
    }

    /*
     * The executePutRequest() helper method processes files for uploading to
     * the target web server and URL.
     */
    protected void executePutRequest(HttpURLConnection httpConnection)
            throws IOException, FileNotFoundException {
        httpConnection.setDoOutput(true);
        OutputStream os = httpConnection.getOutputStream();
        FileInputStream fis = new FileInputStream(fileForPut);
        int i;
        while ((i = fis.read()) != -1) {
            os.write(i);
        }
        os.close();
    }

    /* Getters & Setters */
    public String getTheHttpMethod() {
        return theHttpMethod;
    }

    public String getTheURL() {
        return theURL;
    }

    public void setTheHttpMethod(String string) {
        theHttpMethod = string;
    }

    public void setTheURL(String string) {
        theURL = string;
    }

    public Map getRequestInfo() {
        return requestInfo;
    }

    public ArrayList getResponseBody() {
        return responseBody;
    }

    public Map getResponseInfo() {
        return responseInfo;
    }

    public void setFileForPut(File file) {
        fileForPut = file;
    }

    public void setDataForPost(String string) {
        dataForPost = string;
    }

    public int getResponseCode() {
        return responseCode;
    }

    /*
     * The getTextForResponseCode() method returns the short description for a
     * given numeric response code.
     */
    public String getTextForResponseCode(int responseCode) {

        String responseCodeText;

        Integer i = new Integer(responseCode);
        responseCodeText = (String) responseCodeMappings.get(i);
        if (responseCodeText == null) {
            return "";
        } else {
            return responseCodeText;
        }

    }

    /*
     * The getTypeForHeaderKey() method returns the header field type (request,
     * response, header, entity) for a given header field name.
     */
    public String getTypeForHeaderKey(String key) {
        return (String) headerMappings.get(key);
    }
}